;;; echo-msk.el --- Listen to "Эхо Москвы" from Emacs

;; Copyright (C) 2014 Alex Kost

;; Author: Alex Kost <alezost@gmail.com>
;; Created: 25 Oct 2014
;; URL: https://gitlab.com/alezost-emacs/echo-msk
;; Keywords: multimedia

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <http://www.gnu.org/licenses/>.

;;; Commentary:

;; "Эхо Москвы" is a Russian radio station: <http://www.echo.msk.ru/>.

;; This file provides an interface for:
;;
;; - listening to the latest programs and online audio using EMMS;
;; - browsing (opening with `browse-url') particular web pages
;;   (schedules, program pages, pages with video frames and so on).

;; Some interactive commands:
;;
;; - echo-msk-browse-schedule;
;; - echo-msk-browse-online-video;
;; - echo-msk-browse-online-audio;
;; - echo-msk-emms-play-online-audio.
;;
;; But I think the most useful command is `echo-msk-program-task'.  It
;; may be used to select a particular action on an "Эхо" program (for
;; example, you may choose to listen 3 last programs).

;; This package requires 2 external packages:
;;
;; - EMMS <http://www.gnu.org/software/emms/>;
;; - text-search <https://gitlab.com/alezost-emacs/text-search>.

;; An optional dependency is "emacs-wget" package
;; <http://www.emacswiki.org/emacs/EmacsWget>.  It is used only for
;; downloading an mp3 file (with `echo-msk-wget-recent-mp3' function).

;;; Code:

(require 'cl-lib)
(require 'browse-url)
(require 'emms)
(require 'text-search)

(defgroup echo-msk nil
  "Interface for listening, viewing and browsing \"Эхо Москвы\" radio."
  :group 'multimedia)

(defcustom echo-msk-input-method "russian-computer"
  "Input method to use in minibuffer while prompting for a program."
  :link '(custom-manual "(emacs)Input Methods")
  :type '(choice (const nil)
                 mule-input-method-string)
  :group 'echo-msk)

(defcustom echo-msk-program-functions
  '(echo-msk-emms-add-and-play-count echo-msk-emms-add-count
    echo-msk-wget-recent-mp3 echo-msk-browse-recent-program-video
    echo-msk-browse-recent-program echo-msk-show-recent-programs
    browse-url)
  "List of functions for `echo-msk-program-task'.
Each function is called with a program URL."
  :type '(repeat function)
  :group 'echo-msk)

(defvar echo-msk-domain "echo.msk.ru"
  "Top domain.")

(defvar echo-msk-url (concat "http://www." echo-msk-domain)
  "Root URL of \"Эхо Москвы\".")

(defvar echo-msk-audio-url (concat "http://cdn." echo-msk-domain "/snd")
  "Root URL for audio (mp3) files.")

(defvar echo-msk-online-audio-stream
  "http://81.19.85.197/echo.mp3"
  "URL of the online audio stream.")

(defvar echo-msk-online-audio-url
  (concat echo-msk-url "/sounds/stream.html")
  "URL of the online audio web page.")

(defvar echo-msk-online-video-url
  "http://echomsk.onlinetv.ru/player"
  "URL of the online video web page.")

(defvar echo-msk-mp3-re
  (rx-to-string
   '(and (eval echo-msk-audio-url) "/"
         (= 4 digit) "-"
         (= 2 digit) "-"
         (= 2 digit) "-"
         (+ (any alnum "_")) "-"
         (= 4 digit)
         ".mp3")
   t)
  "Regexp for mp3 files.")

(defvar echo-msk-video-url-re
  (rx-to-string
   '(and "http://" (eval echo-msk-domain)
         "/videos/" (+ digit) ".html")
   t)
  "Regexp for video web pages.")

(defvar echo-msk-buffer-name "*echo-msk*"
  "Buffer name used by `echo-msk-show-recent-programs'.")

(defvar echo-msk-programs
  '(("brother"             . "120 минут классики рока")
    ("year2014"            . "2014")
    ("48minut"             . "48 минут")
    ("96_pages"            . "96 страниц")
    ("my_moscow"           . "«Моя Москва» с...")
    ("autor"               . "Авторская песня")
    ("arsenal"             . "Арсенал")
    ("babnik"              . "Бабник")
    ("korzun"              . "Без дураков")
    ("nomed"               . "Без посредников")
    ("university"          . "Биржевые университеты")
    ("beatles"             . "Битловский час")
    ("blogout1"            . "Блог-аут")
    ("bigecho"             . "Большое Эхо")
    ("zdor"                . "Будем здоровы")
    ("babyboom"            . "Бэби-Бум")
    ("sorokina"            . "В круге СВЕТА")
    ("blues"               . "Весь этот блюз")
    ("vech"                . "Вечерний канал")
    ("vinil"               . "Винил")
    ("vosadu"              . "Во саду ли, в огороде")
    ("vodnayasreda"        . "Водная среда")
    ("voensovet"           . "Военный совет")
    ("vottak"              . "Вот так")
    ("vsetak"              . "Все так")
    ("vyboryasen"          . "Выбор ясен")
    ("galopom"             . "Галопом по Европам")
    ("ganapolskoe_itogi"   . "Ганапольское. Итоги без Евгения Киселева")
    ("garage"              . "Гараж")
    ("speakrus"            . "Говорим по-русски. Передача-игра")
    ("rusalmanach"         . "Говорим по-русски. Радио-альманах")
    ("gorodunderhand"      . "Город за спиной")
    ("gorod_ot_uma"        . "Город от ума")
    ("graniweek"           . "Грани недели")
    ("granit"              . "Гранит науки")
    ("buntman-kid"         . "Детская площадка с папашей Бунтманом")
    ("aav-kid"             . "Детская площадка с папашей Венедиктовым")
    ("gulko-kid"           . "Детская площадка с папашей Гульком")
    ("durnovo-kid"         . "Детская площадка с папашей Дурново")
    ("olevskiy-kid"        . "Детская площадка с папашей Олевским")
    ("jazz"                . "Джаз для коллекционеров")
    ("Diletanti"           . "Дилетанты")
    ("dithyramb"           . "Дифирамб")
    ("daybeat"             . "Дневной обход")
    ("doehali"             . "Доехали")
    ("dorojny_prosvet"     . "Дорога")
    ("Road_map"            . "Дорожная карта")
    ("orjuha"              . "Железный занавес")
    ("asa"                 . "Записки А.С.А.")
    ("animal"              . "Зверсовет")
    ("zoloto"              . "Золотая полка")
    ("beseda"              . "Интервью")
    ("exit"                . "Ищем выход...")
    ("correctly"           . "Как правильно")
    ("truth"               . "Как это было на самом деле")
    ("keys"                . "Кейс")
    ("books"               . "Книжечки")
    ("kazino"              . "Книжное казино")
    ("booknews"            . "Книжные новости")
    ("code"                . "Код доступа")
    ("redrquare"           . "Красная площадь, д.1")
    ("whowhear"            . "Кто куда?")
    ("faraway"             . "Куда подальше?")
    ("kulshok"             . "Культурный шок")
    ("money"               . "Люди и Деньги")
    ("medinfo"             . "Мединфо")
    ("meteoscop"           . "Метеоскоп")
    ("nauka"               . "Москва. Территория науки")
    ("oldmsk"              . "Московские старости")
    ("naukafokus"          . "Наука в фокусе")
    ("netak"               . "Не так")
    ("time"                . "Непрошедшее время")
    ("alekseev"            . "Ночной эфир Бориса Алексеева")
    ("deniok"              . "Ну и денек")
    ("penie"               . "О пении, об опере, о славе")
    ("communication"       . "О связи все и сразу")
    ("style"               . "О стиле все и сразу")
    ("gazuta"              . "Обзор прессы")
    ("oblozhka-1"          . "Обложка-1")
    ("odin"                . "Один")
    ("oni"                 . "Они")
    ("orders"              . "Ордена")
    ("personalno"          . "Особое мнение")
    ("apriscatole"         . "Открывашка")
    ("parking"             . "Парковка")
    ("farm"                . "Письма с фермы")
    ("albac"               . "Полный Альбац")
    ("programm"            . "Программное обеспечение")
    ("progulki"            . "Прогулки по Москве")
    ("part"                . "Проезжая часть")
    ("proehali"            . "Проехали")
    ("moscowtravel"        . "Путешествия по Подмосковью")
    ("radiodetaly"         . "Радиодетали")
    ("razbor_poleta"       . "Разбор полета")
    ("razvorot"            . "Разворот")
    ("razvorot-morning"    . "Разворот (утренний)")
    ("replika-aav"         . "Реплика ААВ")
    ("replika-ganapolskiy" . "Реплика Ганапольского")
    ("repl"                . "Реплика Ореха")
    ("assembly"            . "Родительское собрание")
    ("orekh_osin"          . "Русский и Бомбардир")
    ("soundtrack"          . "Саундтрек")
    ("svoi-glaza"          . "Своими глазами")
    ("house"               . "Свой дом")
    ("gogo"                . "Синхронный шаг")
    ("skaner"              . "Сканер")
    ("sport"               . "Спорт-курьер")
    ("sportchanel"         . "Спортивный канал")
    ("sut"                 . "Суть событий")
    ("tabel"               . "Табель о рангах")
    ("tv"                  . "Телехранитель")
    ("tochka"              . "Точка")
    ("detour"              . "Утренний обход")
    ("insurance"           . "Фактор риска")
    ("footbal"             . "Футбольный клуб")
    ("dream"               . "Хранитель снов")
    ("victory"             . "Цена Победы")
    ("cenapobedy"          . "Цена Революции")
    ("persontv"            . "Человек из телевизора")
    ("help"                . "Чувствительно")
    ("echodrom"            . "Эходром")
    ("echonet"             . "Эхонет")
    ("echonomica"          . "Эхономика"))
  "Alist of programs.
Car is a name of a program used in a program URL.
Cdr is a title of the program.")

(defun echo-msk-program-titles ()
  "Return a list of program titles from `echo-msk-programs'."
  (mapcar #'cdr echo-msk-programs))

(defun echo-msk-program-prompt ()
  "Prompt for and return program name."
  (minibuffer-with-setup-hook
      (lambda () (set-input-method echo-msk-input-method))
    (let ((title (completing-read "Передача Эха: "
                                  (echo-msk-program-titles) nil t)))
      (car (rassoc title echo-msk-programs)))))

(defun echo-msk-program-url-prompt ()
  "Prompt for and return program URL."
  (echo-msk-program-url (echo-msk-program-prompt)))

(defun echo-msk-schedule-url (time)
  "Return a schedule URL for TIME.
TIME should be a time value."
  (concat echo-msk-url "/schedule/"
          (format-time-string "%F" time)
          ".html"))

(defun echo-msk-program-url (name)
  "Return URL of a program by its name from `echo-msk-programs'."
  (concat echo-msk-url "/programs/" name))

(defun echo-msk-search-function (buffer-or-url)
  "Return function for searching depending on BUFFER-OR-URL."
  (if (bufferp buffer-or-url)
      #'search-buffer-for-matches
    #'search-url-for-matches))

(defun echo-msk-get-mp3-list (buffer-or-url &optional count)
  "Return list of COUNT mp3 files from BUFFER-OR-URL.
Mp3 files should match `echo-msk-mp3-re'.
If COUNT is nil, return all files from BUFFER-OR-URL.
The list is sorted from the earliest to the recent programs."
  (funcall (echo-msk-search-function buffer-or-url)
           buffer-or-url echo-msk-mp3-re count 0 t))

(defun echo-msk-get-url-list (buffer-or-url &optional count)
  "Return list of URLs with last count of programs from BUFFER-OR-URL.
If COUNT is nil, return all programs from this URL.
The list is sorted from the earliest to the recent programs."
  (mapcar (lambda (last-part)
            (concat echo-msk-url last-part))
          (funcall (echo-msk-search-function buffer-or-url)
                   buffer-or-url "href=\"\\(/programs/.+-echo/\\)\""
                   count 1 t)))

(defun echo-msk-prompt-count ()
  "Prompt for the count of mp3 files."
  (read-number "How many mp3 files?: " 1))

(defun echo-msk-prompt-recent-number ()
  "Prompt for the 'recent number'.
1 means the last program."
  (read-number "The number of a recent program: " 1))

;;;###autoload
(defun echo-msk-browse-program-url (name)
  "Browse URL with a NAME program.
NAME should be taken from `echo-msk-programs' variable."
  (interactive (list (echo-msk-program-prompt)))
  (browse-url (echo-msk-program-url name)))

(declare-function org-read-date "org" t)

;;;###autoload
(defun echo-msk-browse-schedule (&optional date)
  "Open a program schedule for a DATE.
DATE should be a time value. If it is nil, use current date.
Interactively, prompt for DATE."
  (interactive (list (org-read-date nil t)))
  (require 'org)
  (browse-url (echo-msk-schedule-url (or date (current-time)))))

;;;###autoload
(defun echo-msk-browse-online-video ()
  "Browse online video."
  (interactive)
  (browse-url-default-browser echo-msk-online-video-url))

;;;###autoload
(defun echo-msk-browse-online-audio ()
  "Browse online audio."
  (interactive)
  (browse-url echo-msk-online-audio-url))

(declare-function emms-play-url "emms-source-file" (url))

;;;###autoload
(defun echo-msk-emms-play-online-audio ()
  "Play online audio with EMMS."
  (interactive)
  (emms-play-url echo-msk-online-audio-stream))

(defun echo-msk-emms-add (url &optional count)
  "Add COUNT mp3 files from URL to EMMS playlist."
  (interactive
   (list (echo-msk-program-url-prompt)
         (echo-msk-prompt-count)))
  (mapc (lambda (mp3)
          (emms-source-add 'emms-source-url mp3))
        (echo-msk-get-mp3-list url count)))

(defun echo-msk-emms-add-and-play (url &optional count)
  "Add COUNT mp3 files from URL to EMMS playlist and play."
  (interactive
   (list (echo-msk-program-url-prompt)
         (echo-msk-prompt-count)))
  (with-current-emms-playlist
    (let ((first-new-track (point-max)))
      (echo-msk-emms-add url count)
      (emms-playlist-select first-new-track)))
  (emms-stop)
  (emms-start))

(defun echo-msk-emms-add-count (url)
  "Add mp3 files from URL to EMMS playlist.
Prompt for the count of files."
  (echo-msk-emms-add url (echo-msk-prompt-count)))

(defun echo-msk-emms-add-and-play-count (url)
  "Add mp3 files from URL to EMMS playlist and play.
Prompt for the count of files."
  (echo-msk-emms-add-and-play url (echo-msk-prompt-count)))

(defun echo-msk-recent-program-url (url)
  "Return url of a recent program from a program URL.
Prompt for the 'recent number'."
  (car (echo-msk-get-url-list url (echo-msk-prompt-recent-number))))

(defun echo-msk-recent-program-mp3 (url)
  "Return mp3 of a recent program from a program URL.
Prompt for the 'recent number'."
  (car (echo-msk-get-mp3-list url (echo-msk-prompt-recent-number))))

(defun echo-msk-program-video-url (url)
  "Return url of a video frame from URL with a particular program."
  (car (search-url-for-matches url echo-msk-video-url-re 1)))

(defun echo-msk-browse-recent-program (url)
  "Browse a page with a recent program."
  (browse-url (echo-msk-recent-program-url url)))

(defun echo-msk-browse-recent-program-video (url)
  "Browse a page with a video frame of recent program."
  (browse-url-default-browser
   (echo-msk-program-video-url (echo-msk-recent-program-url url))))

(declare-function wget "wget" (url &optional arg))

(defun echo-msk-wget-recent-mp3 (url)
  "Download recent mp3 file from program URL with `wget'."
  (wget (echo-msk-recent-program-mp3 url)))

(defun echo-msk-show-recent-programs (url)
  "Display recent program urls and mp3s by a program URL."
  (let* ((url-buf (url-retrieve-synchronously url))
         (urls (echo-msk-get-url-list url-buf))
         (mp3s (echo-msk-get-mp3-list url-buf))
         (buf (get-buffer-create echo-msk-buffer-name)))
    (with-current-buffer buf
      (setq buffer-read-only nil)
      (erase-buffer)
      (insert url "\n\n")
      (cl-mapc (lambda (url mp3)
                 (insert url "\t" mp3 "\n"))
               urls mp3s))
    (switch-to-buffer buf)))

;;;###autoload
(defun echo-msk-program-task (url fun)
  "Apply a function FUN to URL with program.
Interactively, prompt for URL (or a program) and for a function
from `echo-msk-program-functions'."
  (interactive
   (list (echo-msk-program-url-prompt)
         (intern
          (completing-read "Function: "
                           (mapcar #'symbol-name
                                   echo-msk-program-functions)
                           nil t))))
  (funcall fun url))

(provide 'echo-msk)

;;; echo-msk.el ends here
